/** * Copyright (C) 2010-2017 Structr GmbH * * This file is part of Structr <http://structr.org>. * * Structr is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * Structr is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Structr. If not, see <http://www.gnu.org/licenses/>. */ package org.structr.rest.service; import ch.qos.logback.access.jetty.RequestLogImpl; import ch.qos.logback.access.servlet.TeeFilter; import java.io.File; import java.io.IOException; import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.DispatcherType; import javax.servlet.http.HttpServlet; import org.apache.chemistry.opencmis.server.impl.CmisRepositoryContextListener; import org.apache.chemistry.opencmis.server.impl.atompub.CmisAtomPubServlet; import org.apache.chemistry.opencmis.server.impl.browser.CmisBrowserBindingServlet; import org.apache.chemistry.opencmis.server.shared.BasicAuthCallContextHandler; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandlerCollection; import org.eclipse.jetty.server.handler.DefaultHandler; import org.eclipse.jetty.server.handler.HandlerCollection; import org.eclipse.jetty.server.handler.RequestLogHandler; import org.eclipse.jetty.server.handler.ResourceHandler; import org.eclipse.jetty.server.session.HashSessionManager; import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlets.AsyncGzipFilter; import org.eclipse.jetty.servlets.GzipFilter; import org.eclipse.jetty.util.resource.JarResource; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceCollection; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.structr.api.config.Settings; import org.structr.api.service.Command; import org.structr.api.service.RunnableService; import org.structr.api.service.StructrServices; import org.structr.core.Services; import org.structr.rest.ResourceProvider; import org.tuckey.web.filters.urlrewrite.UrlRewriteFilter; /** * * */ public class HttpService implements RunnableService { private static final Logger logger = LoggerFactory.getLogger(HttpService.class.getName()); // set of resource providers for this service private final Set<ResourceProvider> resourceProviders = new LinkedHashSet<>(); private enum LifecycleEvent { Started, Stopped } private HashSessionManager hashSessionManager = null; private HttpConfiguration httpConfig = null; private HttpConfiguration httpsConfig = null; private Server server = null; private int maxIdleTime = 30000; private int requestHeaderSize = 8192; @Override public void startService() throws Exception { logger.info("Starting {} (host={}:{}, maxIdleTime={}, requestHeaderSize={})", Settings.ApplicationTitle.getValue(), Settings.ApplicationHost.getValue(), Settings.HttpPort.getValue(), maxIdleTime, requestHeaderSize); logger.info("Base path {}", Settings.getBasePath()); logger.info("{} started at http://{}:{}", Settings.ApplicationTitle.getValue(), Settings.ApplicationHost.getValue(), Settings.HttpPort.getValue()); server.start(); try { while (!server.isStarted()) { Thread.sleep(100); } } catch (Throwable t) { t.printStackTrace(); } // The jsp directory is created by the container, but we don't need it removeDir(Settings.getBasePath(), "jsp"); // send lifecycle event that the server has been started sendLifecycleEvent(LifecycleEvent.Started); } @Override public void stopService() { if (server != null) { try { server.stop(); } catch (Exception ex) { logger.warn("Exception while stopping Jetty: {}", ex.getMessage()); } } } @Override public boolean runOnStartup() { return true; } @Override public boolean isRunning() { return server != null && server.isRunning(); } @Override public void injectArguments(Command command) { } @Override public void initialize(final StructrServices services) throws ClassNotFoundException, IllegalAccessException, InstantiationException { String sourceJarName = getClass().getProtectionDomain().getCodeSource().getLocation().toString(); final boolean isTest = Settings.Testing.getValue(); if (!isTest && StringUtils.stripEnd(sourceJarName, System.getProperty("file.separator")).endsWith("classes")) { String jarFile = System.getProperty("jarFile"); if (StringUtils.isEmpty(jarFile)) { throw new IllegalArgumentException(getClass().getName() + " was started in an environment where the classloader cannot determine the JAR file containing the main class.\n" + "Please specify the path to the JAR file in the parameter -DjarFile.\n" + "Example: -DjarFile=${project.build.directory}/${project.artifactId}-${project.version}.jar"); } sourceJarName = jarFile; } // load configuration from properties file maxIdleTime = Services.parseInt(System.getProperty("maxIdleTime"), 30000); requestHeaderSize = Services.parseInt(System.getProperty("requestHeaderSize"), 8192); if (Settings.Async.getValue()) { logger.info("Running in asynchronous mode"); } // other properties final String keyStorePath = Settings.KeystorePath.getValue(); final String keyStorePassword = Settings.KeystorePassword.getValue(); final String contextPath = System.getProperty("contextPath", "/"); final boolean enableHttps = Settings.HttpsEnabled.getValue(); final boolean enableGzipCompression = Settings.GzipCompression.getValue(); final boolean logRequests = Settings.RequestLogging.getValue(); final String logPrefix = Settings.LogPrefix.getValue(); final String host = Settings.ApplicationHost.getValue(); final int httpsPort = Settings.HttpsPort.getValue(); boolean enableRewriteFilter = true; // get current base path String basePath = System.getProperty("home", Settings.getBasePath()); if (StringUtils.isEmpty(basePath)) { // use cwd and, if that fails, /tmp as a fallback basePath = System.getProperty("user.dir", "/tmp"); } // create base directory if it does not exist final File baseDir = new File(basePath); if (!baseDir.exists()) { baseDir.mkdirs(); } server = new Server(Settings.HttpPort.getValue()); final ContextHandlerCollection contexts = new ContextHandlerCollection(); contexts.addHandler(new DefaultHandler()); final ServletContextHandler servletContext = new ServletContextHandler(server, contextPath, true, true); final List<Connector> connectors = new LinkedList<>(); // create resource collection from base path & source JAR try { servletContext.setBaseResource(new ResourceCollection(Resource.newResource(basePath), JarResource.newJarResource(Resource.newResource(sourceJarName)))); } catch (Throwable t) { logger.warn("Base resource {} not usable: {}", new Object[]{basePath, t.getMessage()}); } // this is needed for the filters to work on the root context "/" servletContext.addServlet("org.eclipse.jetty.servlet.DefaultServlet", "/"); servletContext.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false"); if (Settings.ConfigServletEnabled.getValue()) { // configuration wizard entry point servletContext.addServlet("org.structr.rest.servlet.ConfigServlet", "/structr/config/*"); } // CMIS setup if (Settings.CmisEnabled.getValue()) { try { servletContext.addEventListener(new CmisRepositoryContextListener()); final ServletHolder cmisAtomHolder = servletContext.addServlet(CmisAtomPubServlet.class.getName(), "/structr/cmis/atom/*"); cmisAtomHolder.setInitParameter("callContextHandler", BasicAuthCallContextHandler.class.getName()); cmisAtomHolder.setInitParameter("cmisVersion", "1.1"); final ServletHolder cmisBrowserHolder = servletContext.addServlet(CmisBrowserBindingServlet.class.getName(), "/structr/cmis/browser/*"); cmisBrowserHolder.setInitParameter("callContextHandler", BasicAuthCallContextHandler.class.getName()); cmisBrowserHolder.setInitParameter("cmisVersion", "1.1"); } catch (Throwable t) { logger.warn("Cannot initialize CMIS servlet", t); } } hashSessionManager = new HashSessionManager(); try { hashSessionManager.setStoreDirectory(new File(baseDir + "/sessions")); } catch (IOException ex) { logger.warn("Could not set custom session manager with session store directory {}/sessions", baseDir); } servletContext.getSessionHandler().setSessionManager(hashSessionManager); if (enableRewriteFilter) { final FilterHolder rewriteFilter = new FilterHolder(UrlRewriteFilter.class); rewriteFilter.setInitParameter("confPath", "urlrewrite.xml"); servletContext.addFilter(rewriteFilter, "/*", EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.ASYNC)); } if (enableGzipCompression) { final FilterHolder gzipFilter = Settings.Async.getValue() ? new FilterHolder(AsyncGzipFilter.class) : new FilterHolder(GzipFilter.class); gzipFilter.setInitParameter("mimeTypes", "text/html,text/plain,text/css,text/javascript,application/json"); gzipFilter.setInitParameter("bufferSize", "32768"); gzipFilter.setInitParameter("minGzipSize", "256"); gzipFilter.setInitParameter("deflateCompressionLevel", "9"); gzipFilter.setInitParameter("methods", "GET,POST,PUT,HEAD,DELETE"); servletContext.addFilter(gzipFilter, "/*", EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.ASYNC)); } contexts.addHandler(servletContext); // enable request logging if (logRequests) { final String etcPath = basePath + "/etc"; final File etcDir = new File(etcPath); if (!etcDir.exists()) { etcDir.mkdir(); } final String logbackConfFilePath = basePath + "/etc/logback-access.xml"; final File logbackConfFile = new File(logbackConfFilePath); if (!logbackConfFile.exists()) { // synthesize a logback accees log config file List<String> config = new LinkedList<>(); config.add("<configuration>"); config.add(" <appender name=\"FILE\" class=\"ch.qos.logback.core.rolling.RollingFileAppender\">"); config.add(" <rollingPolicy class=\"ch.qos.logback.core.rolling.TimeBasedRollingPolicy\">"); config.add(" <fileNamePattern>logs/" + logPrefix + "-%d{yyyy_MM_dd}.request.log.zip</fileNamePattern>"); config.add(" </rollingPolicy>"); config.add(" <encoder>"); config.add(" <charset>UTF-8</charset>"); config.add(" <pattern>%h %l %u %t \"%r\" %s %b %n%fullRequest%n%n%fullResponse</pattern>"); config.add(" </encoder>"); config.add(" </appender>"); config.add(" <appender-ref ref=\"FILE\" />"); config.add("</configuration>"); try { logbackConfFile.createNewFile(); FileUtils.writeLines(logbackConfFile, "UTF-8", config); } catch (IOException ioex) { logger.warn("Unable to write logback configuration.", ioex); } } final FilterHolder loggingFilter = new FilterHolder(TeeFilter.class); servletContext.addFilter(loggingFilter, "/*", EnumSet.of(DispatcherType.REQUEST, Settings.Async.getValue() ? DispatcherType.ASYNC : DispatcherType.FORWARD)); loggingFilter.setInitParameter("includes", ""); final RequestLogHandler requestLogHandler = new RequestLogHandler(); final String logPath = basePath + "/logs"; final File logDir = new File(logPath); // Create logs directory if not existing if (!logDir.exists()) { logDir.mkdir(); } final RequestLogImpl requestLog = new RequestLogImpl(); requestLog.setName("REQUESTLOG"); requestLogHandler.setRequestLog(requestLog); final HandlerCollection handlers = new HandlerCollection(); handlers.setHandlers(new Handler[]{contexts, requestLogHandler}); server.setHandler(handlers); } else { server.setHandler(contexts); } final List<ContextHandler> resourceHandler = collectResourceHandlers(); for (ContextHandler contextHandler : resourceHandler) { contexts.addHandler(contextHandler); } final Map<String, ServletHolder> servlets = collectServlets(); // add servlet elements int position = 1; for (Map.Entry<String, ServletHolder> servlet : servlets.entrySet()) { final ServletHolder servletHolder = servlet.getValue(); final String path = servlet.getKey(); servletHolder.setInitOrder(position++); logger.info("Adding servlet {} for {}", new Object[]{servletHolder, path}); servletContext.addServlet(servletHolder, path); } contexts.addHandler(servletContext); if (StringUtils.isNotBlank(host) && Settings.HttpPort.getValue() > -1) { httpConfig = new HttpConfiguration(); httpConfig.setSecureScheme("https"); httpConfig.setSecurePort(httpsPort); //httpConfig.setOutputBufferSize(8192); httpConfig.setRequestHeaderSize(requestHeaderSize); final ServerConnector httpConnector = new ServerConnector(server, new HttpConnectionFactory(httpConfig)); httpConnector.setHost(host); httpConnector.setPort(Settings.HttpPort.getValue()); connectors.add(httpConnector); } else { logger.warn("Unable to configure HTTP server port, please make sure that {} and {} are set correctly in structr.conf.", Settings.ApplicationHost.getKey(), Settings.HttpPort.getKey()); } if (enableHttps) { if (httpsPort > -1 && keyStorePath != null && !keyStorePath.isEmpty() && keyStorePassword != null) { httpsConfig = new HttpConfiguration(httpConfig); httpsConfig.addCustomizer(new SecureRequestCustomizer()); final SslContextFactory sslContextFactory = new SslContextFactory(); sslContextFactory.setKeyStorePath(keyStorePath); sslContextFactory.setKeyStorePassword(keyStorePassword); final ServerConnector https = new ServerConnector(server, new SslConnectionFactory(sslContextFactory, "http/1.1"), new HttpConnectionFactory(httpsConfig)); https.setPort(httpsPort); https.setIdleTimeout(500000); https.setHost(host); https.setPort(httpsPort); connectors.add(https); } else { logger.warn("Unable to configure SSL, please make sure that {}, {} and {} are set correctly in structr.conf.", new Object[]{ Settings.HttpsPort.getKey(), Settings.KeystorePath.getKey(), Settings.KeystorePassword.getKey() }); } } if (!connectors.isEmpty()) { server.setConnectors(connectors.toArray(new Connector[0])); } else { logger.error("No connectors configured, aborting."); System.exit(0); } server.setStopTimeout(1000); server.setStopAtShutdown(true); } @Override public void initialized() {} @Override public void shutdown() { if (server != null) { try { server.stop(); } catch (Exception ex) { logger.warn("Error while stopping Jetty server: {}", ex.getMessage()); } } // send lifecycle event that the server has been stopped sendLifecycleEvent(LifecycleEvent.Stopped); } @Override public String getName() { return HttpService.class.getName(); } @Override public boolean isVital() { return true; } public Set<ResourceProvider> getResourceProviders() { return resourceProviders; } public HashSessionManager getHashSessionManager() { return hashSessionManager; } // ----- private methods ----- private List<ContextHandler> collectResourceHandlers() throws ClassNotFoundException, InstantiationException, IllegalAccessException { final List<ContextHandler> resourceHandlers = new LinkedList<>(); final String resourceHandlerList = Settings.ResourceHandlers.getValue(); if (resourceHandlerList != null) { for (String resourceHandlerName : resourceHandlerList.split("[ \\t]+")) { if (StringUtils.isNotBlank(resourceHandlerName)) { final String contextPath = Settings.getStringSetting(resourceHandlerName, "contextPath").getValue(); if (contextPath != null) { final String resourceBase = Settings.getStringSetting(resourceHandlerName, "resourceBase").getValue(); if (resourceBase != null) { final ResourceHandler resourceHandler = new ResourceHandler(); resourceHandler.setDirectoriesListed(Settings.getBooleanSetting(resourceHandlerName, "directoriesListed").getValue()); final String welcomeFiles = Settings.getStringSetting(resourceHandlerName, "welcomeFiles").getValue(); if (welcomeFiles != null) { resourceHandler.setWelcomeFiles(StringUtils.split(welcomeFiles)); } resourceHandler.setResourceBase(resourceBase); resourceHandler.setCacheControl("max-age=0"); resourceHandler.setEtags(true); final ContextHandler staticResourceHandler = new ContextHandler(); staticResourceHandler.setContextPath(contextPath); staticResourceHandler.setHandler(resourceHandler); resourceHandlers.add(staticResourceHandler); } else { logger.warn("Unable to register resource handler {}, missing {}.resourceBase", resourceHandlerName, resourceHandlerName); } } else { logger.warn("Unable to register resource handler {}, missing {}.contextPath", resourceHandlerName, resourceHandlerName); } } } } else { logger.warn("No resource handlers configured for HttpService."); } return resourceHandlers; } private Map<String, ServletHolder> collectServlets() throws ClassNotFoundException, InstantiationException, IllegalAccessException { final Map<String, ServletHolder> servlets = new LinkedHashMap<>(); String servletNameList = Settings.Servlets.getValue(); if (servletNameList != null) { for (String servletName : servletNameList.split("[ \\t]+")) { if (StringUtils.isNotBlank(servletName)) { final String servletClassName = Settings.getStringSetting(servletName, "class").getValue(); if (servletClassName != null) { final String servletPath = Settings.getStringSetting(servletName, "path").getValue(); if (servletPath != null) { try { final HttpServlet servlet = (HttpServlet)Class.forName(servletClassName).newInstance(); if (servlet instanceof HttpServiceServlet) { ((HttpServiceServlet)servlet).getConfig().initializeFromSettings(servletName, resourceProviders); } if (servletPath.endsWith("*")) { servlets.put(servletPath, new ServletHolder(servlet)); } else { servlets.put(servletPath + "/*", new ServletHolder(servlet)); } } catch (ClassNotFoundException nfex) { logger.warn("Unable to instantiate servlet class {} for servlet {}", servletClassName, servletName); } } else { logger.warn("Unable to register servlet {}, missing {}.path", servletName, servletName); } } else { logger.warn("Unable to register servlet {}, missing {}.class", servletName, servletName); } } } } else { logger.warn("No servlets configured for HttpService."); } return servlets; } private void removeDir(final String basePath, final String directoryName) { final String strippedBasePath = StringUtils.stripEnd(basePath, "/"); final File file = new File(strippedBasePath + "/" + directoryName); if (file.isDirectory()) { try { FileUtils.deleteDirectory(file); } catch (IOException ex) { logger.error("Unable to delete directory {}: {}", new Object[]{directoryName, ex.getMessage()}); } } else { file.delete(); } } // ----- private methods ----- private void sendLifecycleEvent(final LifecycleEvent event) { // instantiate and call lifecycle callbacks from configuration file final String listeners = Settings.LifecycleListeners.getValue(); if (listeners != null) { final String[] listenerClasses = listeners.split("[\\s ,;]+"); for (String listenerClass : listenerClasses) { if (StringUtils.isNotBlank(listenerClass)) { try { final HttpServiceLifecycleListener listener = (HttpServiceLifecycleListener) Class.forName(listenerClass).newInstance(); switch (event) { case Started: listener.serverStarted(); break; case Stopped: listener.serverStopped(); break; } } catch (InstantiationException | IllegalAccessException | ClassNotFoundException ex) { logger.error("Unable to send lifecycle event to listener " + listenerClass, ex); } } } } } }